package com.getan.boot.module.system.service.biz;

import cn.dev33.satoken.stp.SaLoginConfig;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpInterface;
import com.getan.boot.common.enums.EnableStatus;
import com.getan.boot.common.holder.security.SecurityContextHolder;
import com.getan.boot.common.holder.security.dto.MenuDTO;
import com.getan.boot.common.utils.CollectionUtils;
import com.getan.boot.common.utils.Preconditions;
import com.getan.boot.common.utils.StringUtils;
import com.getan.boot.module.system.constant.Constants;
import com.getan.boot.module.system.constant.ErrorCode;
import com.getan.boot.module.system.constant.PermissionTypeEnum;
import com.getan.boot.module.system.domain.vo.LoginVO;
import com.getan.boot.module.system.entity.PermissionEntity;
import com.getan.boot.module.system.entity.RoleEntity;
import com.getan.boot.module.system.entity.UserEntity;
import com.getan.boot.module.system.service.persistent.PermissionService;
import com.getan.boot.module.system.service.persistent.RoleService;
import com.getan.boot.module.system.service.persistent.UserService;
import com.getan.boot.security.sa.StpAdminUtil;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * 登录模块
 *
 * @author Bruce.Gong
 * @since 1.0.0-SNAPSHOT
 */
@Slf4j
@RequiredArgsConstructor
@Service
@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
public class LoginService implements StpInterface {

    private final UserService userService;
    private final RoleService roleService;
    private final PermissionService permissionService;

    private static final Map<Long, Set<Long>> USER_ROLE_CACHE = new ConcurrentHashMap<>();
    private static final Map<Long, Set<Long>> USER_PERMISSION_CACHE = new ConcurrentHashMap<>();

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        Set<Long> permissionIds = queryRolePermissions(Long.parseLong(loginId.toString()));
        if (CollectionUtils.isEmpty(permissionIds)) {
            return Collections.emptyList();
        }
        if (permissionIds.contains(-1L)) {
            return Collections.singletonList("*");
        }
        return permissionService.queryPermissions(permissionIds).stream()
                .map(PermissionEntity::getPerms)
                .filter(StringUtils::isNotEmpty)
                .distinct()
                .collect(Collectors.toList());
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        List<RoleEntity> roles = queryUserRoles(Long.parseLong(loginId.toString()));
        return roles.stream()
                .map(RoleEntity::getCode)
                .distinct()
                .collect(Collectors.toList());
    }

    public void logout() {
        Long userId = SecurityContextHolder.getCurrentUserId();
        StpAdminUtil.logout();
        USER_ROLE_CACHE.remove(userId);
        USER_PERMISSION_CACHE.remove(userId);
    }

    private List<RoleEntity> queryUserRoles(Long userId) {
        Set<Long> roleIds = USER_ROLE_CACHE.get(userId);
        if (roleIds == null) {
            roleIds = roleService.queryUserRoleIds(userId);
            USER_ROLE_CACHE.put(userId, roleIds);
        }
        if (CollectionUtils.isEmpty(roleIds)) {
            return Collections.emptyList();
        }
        return roleService.listActiveRoles(roleIds);
    }

    /**
     * 如果是系统管理员，则返回 [-1]
     */
    private Set<Long> queryRolePermissions(Long userId) {
        Set<Long> permissionIds = USER_PERMISSION_CACHE.get(userId);
        if (permissionIds == null) {
            List<RoleEntity> roles = queryUserRoles(userId);
            if (CollectionUtils.isEmpty(roles)) {
                return Collections.emptySet();
            }
            if (roles.stream().anyMatch(v -> Constants.ROLE_ADMIN.equals(v.getCode()))) {
                return Collections.singleton(-1L);
            }
            Set<Long> roleIds = roles.stream().map(RoleEntity::getId).collect(Collectors.toSet());
            permissionIds = permissionService.queryRolePermissionIds(roleIds);
            USER_PERMISSION_CACHE.put(userId, permissionIds);
        }
        return permissionIds;
    }

    public LoginVO doLogin(String username, String password) {
        UserEntity db = userService.getUserByUsername(username);
        Preconditions.isNull(db, ErrorCode.LOGIN_FAIL);
        Preconditions.check(() -> EnableStatus.DISABLE.match(db.getStatus()), ErrorCode.USER_DISABLED);
        String encodePassword = DigestUtils.sha256Hex(password);
        Preconditions.check(() -> !Objects.equals(db.getPassword(), encodePassword), ErrorCode.LOGIN_FAIL);
        StpAdminUtil.login(db.getId(), loginExtraData(db));
        return loginInfo(db.getId());
    }

    private SaLoginModel loginExtraData(UserEntity user) {
        Map<String, Object> map = Maps.newHashMapWithExpectedSize(8);
        map.put("username", user.getUsername());
        map.put("nickname", user.getNickname());
        map.put("mobile", user.getMobile());
        map.put("avatar", user.getAvatar());
        return SaLoginConfig.setExtraData(map);
    }

    public LoginVO loginInfo(Long userId) {
        Set<String> permissionCodes = new HashSet<>();
        List<MenuDTO> permissions = Collections.emptyList();
        Set<Long> permissionIds = queryRolePermissions(userId);
        if (CollectionUtils.isNotEmpty(permissionIds)) {
            if (permissionIds.contains(-1L)) {
                permissionCodes.add("*");
                permissions = permissionService.queryPermissions().stream()
                        .filter(v -> PermissionTypeEnum.MENU.match(v.getType()))
                        .sorted(Comparator.comparing(PermissionEntity::getPid)
                                .thenComparing(PermissionEntity::getSort))
                        .map(this::buildMenu)
                        .collect(Collectors.toList());
            } else {
                List<PermissionEntity> permissionEntities = permissionService.queryPermissions(permissionIds);
                if (CollectionUtils.isNotEmpty(permissionEntities)) {
                    permissions = permissionEntities.stream()
                            .peek(v -> {
                                if (PermissionTypeEnum.BUTTON.match(v.getType()) && StringUtils.isNotEmpty(v.getPerms())) {
                                    permissionCodes.add(v.getPerms());
                                }
                            })
                            .filter(v -> PermissionTypeEnum.MENU.match(v.getType()))
                            .sorted(Comparator.comparing(PermissionEntity::getPid)
                                    .thenComparing(PermissionEntity::getSort))
                            .map(this::buildMenu)
                            .collect(Collectors.toList());
                }
            }
        }
        return LoginVO.builder()
                .username((String) StpAdminUtil.getExtra("username"))
                .nickname((String) StpAdminUtil.getExtra("nickname"))
                .mobile((String) StpAdminUtil.getExtra("mobile"))
                .avatar((String) StpAdminUtil.getExtra("avatar"))
                .token(StpAdminUtil.getTokenValue())
                .permissions(permissionCodes)
                .menus(CollectionUtils.listTree(permissions, MenuDTO::getId, MenuDTO::getPid, 0L, MenuDTO::getChildren, MenuDTO::setChildren))
                .build();
    }

    private MenuDTO buildMenu(PermissionEntity permission) {
        return MenuDTO.builder()
                .id(permission.getId())
                .pid(permission.getPid())
                .name(permission.getName())
                .icon(permission.getIcon())
                .path(permission.getPath())
                .component(permission.getComponent())
                .code(permission.getPerms())
                .show(permission.getVisible())
                .meta(permission.getMeta())
                .build();
    }

}
